<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->


<bindings id="menulistBindings"
   xmlns="http://www.mozilla.org/xbl"
   xmlns:html="http://www.w3.org/1999/xhtml"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="menulist-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
    <resources>
      <stylesheet src="chrome://global/content/menulist.css"/>
      <stylesheet src="chrome://global/skin/menulist.css"/>
    </resources>
  </binding>

  <binding id="menulist" display="xul:menu" role="xul:menulist"
           extends="chrome://global/content/bindings/menulist.xml#menulist-base">
    <content sizetopopup="pref">
      <xul:hbox class="menulist-label-box" flex="1">
        <xul:image class="menulist-icon" xbl:inherits="src=image,src"/>
        <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/>
      </xul:hbox>
      <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
      <children includes="menupopup"/>
    </content>

    <handlers>
      <handler event="command" phase="capturing"
        action="if (event.target.parentNode.parentNode == this) this.selectedItem = event.target;"/>

      <handler event="popupshowing">
        <![CDATA[
          if (event.target.parentNode == this) {
            this.menuBoxObject.activeChild = null;
            if (this.selectedItem)
              // Not ready for auto-setting the active child in hierarchies yet.
              // For now, only do this when the outermost menupopup opens.
              this.menuBoxObject.activeChild = this.mSelectedInternal;
          }
        ]]>
      </handler>

      <handler event="keypress" modifiers="shift any" group="system">
        <![CDATA[
          if (!event.defaultPrevented &&
              (event.keyCode == KeyEvent.DOM_VK_UP ||
               event.keyCode == KeyEvent.DOM_VK_DOWN ||
               event.keyCode == KeyEvent.DOM_VK_PAGE_UP ||
               event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN ||
               event.keyCode == KeyEvent.DOM_VK_HOME ||
               event.keyCode == KeyEvent.DOM_VK_END ||
               event.keyCode == KeyEvent.DOM_VK_BACK_SPACE ||
               event.charCode > 0)) {
            // Moving relative to an item: start from the currently selected item
            this.menuBoxObject.activeChild = this.mSelectedInternal;
            if (this.menuBoxObject.handleKeyPress(event)) {
              this.menuBoxObject.activeChild.doCommand();
              event.preventDefault();
            }
          }
        ]]>
      </handler>
    </handlers>

    <implementation implements="nsIDOMXULMenuListElement">
      <constructor>
        this.mInputField = null;
        this.mSelectedInternal = null;
        this.mAttributeObserver = null;
        this.menuBoxObject = this.boxObject;
        this.setInitialSelection();
      </constructor>

      <method name="setInitialSelection">
        <body>
          <![CDATA[
            var popup = this.menupopup;
            if (popup) {
              var arr = popup.getElementsByAttribute('selected', 'true');

              var editable = this.editable;
              var value = this.value;
              if (!arr.item(0) && value)
                arr = popup.getElementsByAttribute(editable ? 'label' : 'value', value);

              if (arr.item(0))
                this.selectedItem = arr[0];
              else if (!editable)
                this.selectedIndex = 0;
            }
          ]]>
        </body>
      </method>

      <property name="value" onget="return this.getAttribute('value');">
        <setter>
          <![CDATA[
            // if the new value is null, we still need to remove the old value
            if (val == null)
              return this.selectedItem = val;

            var arr = null;
            var popup = this.menupopup;
            if (popup)
              arr = popup.getElementsByAttribute('value', val);

            if (arr && arr.item(0))
              this.selectedItem = arr[0];
            else {
              this.selectedItem = null;
              this.setAttribute('value', val);
            }

            return val;
          ]]>
        </setter>
      </property>

      <property name="inputField" readonly="true" onget="return null;"/>

      <property name="crop" onset="this.setAttribute('crop',val); return val;"
                            onget="return this.getAttribute('crop');"/>
      <property name="image"  onset="this.setAttribute('image',val); return val;"
                              onget="return this.getAttribute('image');"/>
      <property name="label" readonly="true" onget="return this.getAttribute('label');"/>
      <property name="description" onset="this.setAttribute('description',val); return val;"
                                   onget="return this.getAttribute('description');"/>

      <property name="editable" onget="return this.getAttribute('editable') == 'true';">
        <setter>
          <![CDATA[
            if (!val && this.editable) {
              // If we were focused and transition from editable to not editable,
              // focus the parent menulist so that the focus does not get stuck.
              if (this.inputField == document.activeElement)
                window.setTimeout(() => this.focus(), 0);
            }

            this.setAttribute("editable", val);
            return val;
          ]]>
        </setter>
      </property>

      <property name="open" onset="this.menuBoxObject.openMenu(val);
                                   return val;"
                            onget="return this.hasAttribute('open');"/>

      <property name="itemCount" readonly="true"
                onget="return this.menupopup ? this.menupopup.childNodes.length : 0"/>

      <property name="menupopup" readonly="true">
        <getter>
          <![CDATA[
            var popup = this.firstChild;
            while (popup && popup.localName != "menupopup")
              popup = popup.nextSibling;
            return popup;
          ]]>
        </getter>
      </property>

      <method name="contains">
        <parameter name="item"/>
        <body>
          <![CDATA[
            if (!item)
              return false;

            var parent = item.parentNode;
            return (parent && parent.parentNode == this);
          ]]>
        </body>
      </method>

      <property name="selectedIndex">
        <getter>
          <![CDATA[
            // Quick and dirty. We won't deal with hierarchical menulists yet.
            if (!this.selectedItem ||
                !this.mSelectedInternal.parentNode ||
                this.mSelectedInternal.parentNode.parentNode != this)
              return -1;

            var children = this.mSelectedInternal.parentNode.childNodes;
            var i = children.length;
            while (i--)
              if (children[i] == this.mSelectedInternal)
                break;

            return i;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            var popup = this.menupopup;
            if (popup && 0 <= val) {
              if (val < popup.childNodes.length)
                this.selectedItem = popup.childNodes[val];
            }
            else
              this.selectedItem = null;
            return val;
          ]]>
        </setter>
      </property>

      <property name="selectedItem">
        <getter>
          <![CDATA[
            return this.mSelectedInternal;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            var oldval = this.mSelectedInternal;
            if (oldval == val)
              return val;

            if (val && !this.contains(val))
              return val;

            if (oldval) {
              oldval.removeAttribute('selected');
              this.mAttributeObserver.disconnect();
            }

            this.mSelectedInternal = val;
            let attributeFilter = ["value", "label", "image", "description"];
            if (val) {
              val.setAttribute('selected', 'true');
              for (let attr of attributeFilter) {
                if (val.hasAttribute(attr)) {
                  this.setAttribute(attr, val.getAttribute(attr));
                }
                else {
                  this.removeAttribute(attr);
                }
              }

              this.mAttributeObserver = new MutationObserver(this.handleMutation.bind(this));
              this.mAttributeObserver.observe(val, { attributeFilter });
            }
            else {
              for (let attr of attributeFilter) {
                this.removeAttribute(attr);
              }
            }

            var event = document.createEvent("Events");
            event.initEvent("select", true, true);
            this.dispatchEvent(event);

            event = document.createEvent("Events");
            event.initEvent("ValueChange", true, true);
            this.dispatchEvent(event);

            return val;
          ]]>
        </setter>
      </property>

      <method name="handleMutation">
        <parameter name="aRecords"/>
        <body>
          <![CDATA[
            for (let record of aRecords) {
              let t = record.target;
              if (t == this.mSelectedInternal) {
                let attrName = record.attributeName;
                switch (attrName) {
                  case "value":
                  case "label":
                  case "image":
                  case "description":
                    if (t.hasAttribute(attrName)) {
                      this.setAttribute(attrName, t.getAttribute(attrName));
                    }
                    else {
                      this.removeAttribute(attrName);
                    }
                }
              }
            }
          ]]>
        </body>
      </method>

      <method name="getIndexOfItem">
        <parameter name="item"/>
        <body>
        <![CDATA[
          var popup = this.menupopup;
          if (popup) {
            var children = popup.childNodes;
            var i = children.length;
            while (i--)
              if (children[i] == item)
                return i;
          }
          return -1;
        ]]>
        </body>
      </method>

      <method name="getItemAtIndex">
        <parameter name="index"/>
        <body>
        <![CDATA[
          var popup = this.menupopup;
          if (popup) {
            var children = popup.childNodes;
            if (index >= 0 && index < children.length)
              return children[index];
          }
          return null;
        ]]>
        </body>
      </method>

      <method name="appendItem">
        <parameter name="label"/>
        <parameter name="value"/>
        <parameter name="description"/>
        <body>
        <![CDATA[
          const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          var popup = this.menupopup ||
                      this.appendChild(document.createElementNS(XULNS, "menupopup"));
          var item = document.createElementNS(XULNS, "menuitem");
          item.setAttribute("label", label);
          item.setAttribute("value", value);
          if (description)
            item.setAttribute("description", description);

          popup.appendChild(item);
          return item;
        ]]>
        </body>
      </method>

      <method name="insertItemAt">
        <parameter name="index"/>
        <parameter name="label"/>
        <parameter name="value"/>
        <parameter name="description"/>
        <body>
        <![CDATA[
          const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          var popup = this.menupopup ||
                      this.appendChild(document.createElementNS(XULNS, "menupopup"));
          var item = document.createElementNS(XULNS, "menuitem");
          item.setAttribute("label", label);
          item.setAttribute("value", value);
          if (description)
            item.setAttribute("description", description);

          if (index >= 0 && index < popup.childNodes.length)
            popup.insertBefore(item, popup.childNodes[index]);
          else
            popup.appendChild(item);
          return item;
        ]]>
        </body>
      </method>

      <method name="removeItemAt">
        <parameter name="index"/>
        <body>
        <![CDATA[
          var popup = this.menupopup;
          if (popup && 0 <= index && index < popup.childNodes.length) {
            var remove = popup.childNodes[index];
            popup.removeChild(remove);
            return remove;
          }
          return null;
        ]]>
        </body>
      </method>

      <method name="removeAllItems">
        <body>
        <![CDATA[
          this.selectedItem = null;
          var popup = this.menupopup;
          if (popup)
            this.removeChild(popup);
        ]]>
        </body>
      </method>

      <destructor>
        <![CDATA[
          if (this.mAttributeObserver) {
            this.mAttributeObserver.disconnect();
          }
        ]]>
      </destructor>
    </implementation>
  </binding>

  <binding id="menulist-editable" extends="chrome://global/content/bindings/menulist.xml#menulist">
    <content sizetopopup="pref">
      <xul:hbox class="menulist-editable-box textbox-input-box" xbl:inherits="context,disabled,readonly,focused" flex="1">
        <html:input class="menulist-editable-input" anonid="input" allowevents="true"
                    xbl:inherits="value=label,value,disabled,tabindex,readonly,placeholder"/>
      </xul:hbox>
      <xul:dropmarker class="menulist-dropmarker" type="menu"
                      xbl:inherits="open,disabled,parentfocused=focused"/>
      <children includes="menupopup"/>
    </content>

    <implementation>
      <method name="_selectInputFieldValueInList">
        <body>
        <![CDATA[
          if (this.hasAttribute("disableautoselect"))
            return;

          // Find and select the menuitem that matches inputField's "value"
          var arr = null;
          var popup = this.menupopup;

          if (popup)
            arr = popup.getElementsByAttribute('label', this.inputField.value);

          this.setSelectionInternal(arr ? arr.item(0) : null);
        ]]>
        </body>
      </method>

      <method name="setSelectionInternal">
        <parameter name="val"/>
        <body>
          <![CDATA[
            // This is called internally to set selected item
            //  without triggering infinite loop
            //  when using selectedItem's setter
            if (this.mSelectedInternal == val)
              return val;

            if (this.mSelectedInternal)
              this.mSelectedInternal.removeAttribute('selected');

            this.mSelectedInternal = val;

            if (val)
              val.setAttribute('selected', 'true');

            // Do NOT change the "value", which is owned by inputField
            return val;
          ]]>
        </body>
      </method>

      <property name="inputField" readonly="true">
        <getter><![CDATA[
          if (!this.mInputField)
            this.mInputField = document.getAnonymousElementByAttribute(this, "anonid", "input");
          return this.mInputField;
        ]]></getter>
      </property>

      <property name="label"      onset="this.inputField.value = val; return val;"
                                  onget="return this.inputField.value;"/>

      <property name="value"      onget="return this.inputField.value;">
        <setter>
        <![CDATA[
          // Override menulist's value setter to refer to the inputField's value
          // (Allows using "menulist.value" instead of "menulist.inputField.value")
          this.inputField.value = val;
          this.setAttribute('value', val);
          this.setAttribute('label', val);
          this._selectInputFieldValueInList();
          return val;
        ]]>
        </setter>
      </property>

      <property name="selectedItem">
        <getter>
          <![CDATA[
            // Make sure internally-selected item
            //  is in sync with inputField.value
            this._selectInputFieldValueInList();
            return this.mSelectedInternal;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            var oldval = this.mSelectedInternal;
            if (oldval == val)
              return val;

            if (val && !this.contains(val))
              return val;

            // This doesn't touch inputField.value or "value" and "label" attributes
            this.setSelectionInternal(val);
            if (val) {
              // Editable menulist uses "label" as its "value"
              var label = val.getAttribute('label');
              this.inputField.value = label;
              this.setAttribute('value', label);
              this.setAttribute('label', label);
            }
            else {
              this.inputField.value = "";
              this.removeAttribute('value');
              this.removeAttribute('label');
            }

            var event = document.createEvent("Events");
            event.initEvent("select", true, true);
            this.dispatchEvent(event);

            event = document.createEvent("Events");
            event.initEvent("ValueChange", true, true);
            this.dispatchEvent(event);

            return val;
          ]]>
        </setter>
      </property>
      <property name="disableautoselect"
                onset="if (val) this.setAttribute('disableautoselect','true');
                       else this.removeAttribute('disableautoselect'); return val;"
                onget="return this.hasAttribute('disableautoselect');"/>

      <property name="editor" readonly="true">
        <getter><![CDATA[
          const nsIDOMNSEditableElement = Components.interfaces.nsIDOMNSEditableElement;
          return this.inputField.QueryInterface(nsIDOMNSEditableElement).editor;
        ]]></getter>
      </property>

      <property name="readOnly"   onset="this.inputField.readOnly = val;
                                         if (val) this.setAttribute('readonly', 'true');
                                         else this.removeAttribute('readonly'); return val;"
                                  onget="return this.inputField.readOnly;"/>

      <method name="select">
        <body>
          this.inputField.select();
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="focus" phase="capturing">
        <![CDATA[
          this.setAttribute('focused', 'true');
        ]]>
      </handler>

      <handler event="blur" phase="capturing">
        <![CDATA[
          this.removeAttribute('focused');
        ]]>
      </handler>

      <handler event="popupshowing">
        <![CDATA[
          // editable menulists elements aren't in the focus order,
          // so when the popup opens we need to force the focus to the inputField
          if (event.target.parentNode == this) {
            if (document.commandDispatcher.focusedElement != this.inputField)
              this.inputField.focus();

            this.menuBoxObject.activeChild = null;
            if (this.selectedItem)
              // Not ready for auto-setting the active child in hierarchies yet.
              // For now, only do this when the outermost menupopup opens.
              this.menuBoxObject.activeChild = this.mSelectedInternal;
          }
        ]]>
      </handler>

      <handler event="keypress">
        <![CDATA[
          // open popup if key is up arrow, down arrow, or F4
          if (!event.ctrlKey && !event.shiftKey) {
            if (event.keyCode == KeyEvent.DOM_VK_UP ||
                event.keyCode == KeyEvent.DOM_VK_DOWN ||
                (event.keyCode == KeyEvent.DOM_VK_F4 && !event.altKey)) {
              event.preventDefault();
              this.open = true;
            }
          }
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="menulist-description" display="xul:menu"
           extends="chrome://global/content/bindings/menulist.xml#menulist">
    <content sizetopopup="pref">
      <xul:hbox class="menulist-label-box" flex="1">
        <xul:image class="menulist-icon" xbl:inherits="src=image,src"/>
        <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/>
        <xul:label class="menulist-label menulist-description" xbl:inherits="value=description" crop="right" flex="10000"/>
      </xul:hbox>
      <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
      <children includes="menupopup"/>
    </content>
  </binding>

  <binding id="menulist-popuponly" display="xul:menu"
           extends="chrome://global/content/bindings/menulist.xml#menulist">
    <content sizetopopup="pref">
      <children includes="menupopup"/>
    </content>
  </binding>
</bindings>
